package com.jxpanda.spring.module.wechat.component.message;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.jxpanda.infrastructure.crypto.algorithm.digest.ShaDigest;
import com.jxpanda.infrastructure.crypto.algorithm.encrypt.AesEncrypt;
import com.jxpanda.infrastructure.crypto.constant.AlgorithmDigest;
import com.jxpanda.infrastructure.crypto.constant.AlgorithmEncrypt;
import com.jxpanda.infrastructure.crypto.toolkit.KeyLoadKit;
import com.jxpanda.infrastructure.crypto.toolkit.digest.ShaKit;
import com.jxpanda.infrastructure.crypto.toolkit.encrypt.AesKit;
import com.jxpanda.spring.module.wechat.component.message.pojo.WechatXmlMessage;
import com.jxpanda.spring.module.wechat.config.properties.WechatConfigProperties;
import com.jxpanda.spring.module.wechat.exception.WechatCallbackErrorCode;
import com.jxpanda.spring.module.wechat.exception.WechatCallbackException;
import com.jxpanda.spring.module.wechat.toolkit.CorpMessageKit;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * FIXME: 这个类是用来处理微信的回调相关过程的
 *   目前暂时只用在企业微信这里，所以整个实现实际上是企业微信的
 *   由于计划封装的是整个微信相关产品的实现，这里暂时命名为WechatCallbackProcessor
 *   日后如果有需求要实现公众号/小程序等回调业务的时候，查阅微信官方文档后，再来考虑要不要把这个类改名为WechatCorpCallbackProcessor
 *
 * @param appName      应用名，这个参数是内部管理使用的
 *                     用于区分多个应用的消息
 * @param msgSignature 微信消息签名
 * @param timestamp    时间戳
 * @param nonce        随机字符串
 */
@Slf4j
@Builder
public record WechatCallbackProcessor(String appName, String msgSignature, String timestamp, String nonce) {


    private static final AlgorithmEncrypt.AES ALGORITHM = AlgorithmEncrypt.AES.AES_CBC;

    /**
     * 获取指定应用的AES密钥。
     * 该方法用于从配置中读取指定微信应用的回调加密AES密钥。
     *
     * @return 返回指定应用的回调加密AES密钥字符串。
     */
    private String aesKey() {
        // 从微信配置属性中获取当前应用的AES密钥
        // FIXME: 不知道微信官方的示例中为什么要加上一个等号“=”
        //   实测，没有这个等号“=”，就直接报错
        return WechatConfigProperties.wechatConfigProperties().corp().applets().get(appName()).getCallbackEncodingAESKey() + "=";
    }

    /**
     * 获取指定小程序的回调令牌。
     * 该方法不接受任何参数，它通过读取配置文件中指定小程序的配置信息，
     * 来获取该小程序的回调令牌（callbackToken）。
     *
     * @return 返回指定小程序的回调令牌字符串。
     */
    private String token() {
        // 从配置文件中读取当前应用的微信配置，然后获取小程序配置，最后获取指定小程序的回调令牌
        return WechatConfigProperties.wechatConfigProperties().corp().applets().get(appName()).getCallbackToken();
    }

    /**
     * 获取企业ID。
     * 该方法用于从微信配置属性中提取企业的ID。
     *
     * @return 返回从WechatConfigProperties中读取到的企业ID。
     */
    private String corpId() {
        // 从微信配置属性中获取企业标识
        return WechatConfigProperties.wechatConfigProperties().corp().corpId();
    }

    /**
     * 加密内容
     *
     * @param content 需要加密的内容
     * @return 加密后的字符串
     */
    public String encrypt(String content) {
        byte[] randomStrBytes = CorpMessageKit.generateRandomString().getBytes(StandardCharsets.UTF_8);
        byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
        byte[] networkBytesOrder = CorpMessageKit.getNetworkBytesOrder(contentBytes.length);
        byte[] receiveIdBytes = corpId().getBytes(StandardCharsets.UTF_8);
        byte[] paddingBytes = CorpMessageKit.pkcs7PaddingBytes(randomStrBytes.length + contentBytes.length + networkBytesOrder.length + receiveIdBytes.length);
        byte[] unencryptedBytes = ByteBuffer.allocate(randomStrBytes.length + networkBytesOrder.length + contentBytes.length + receiveIdBytes.length + paddingBytes.length)
                .put(randomStrBytes)
                .put(networkBytesOrder)
                .put(contentBytes)
                .put(receiveIdBytes)
                .put(paddingBytes)
                .array();

        return AesKit.builder(ALGORITHM)
                .keyBase64(aesKey())
                .build()
                .encrypt(unencryptedBytes)
                .toBase64();
    }

    public String replyMessage(WechatXmlMessage message) {
        try {
            XmlMapper xmlMapper = new XmlMapper();
            String xmlMessage = xmlMapper.writeValueAsString(message);
            log.info("[replyMessage] xmlMessage: {}", xmlMessage);
            String encryptMessage = encrypt(xmlMessage);
            log.info("[replyMessage] encryptMessage: {}", encryptMessage);
            String signature = signature(encryptMessage);
            log.info("[replyMessage] signature: {}", signature);
            WechatCallbackMessage.ReplyWrapper replyWrapper = new WechatCallbackMessage.ReplyWrapper(encryptMessage, signature, timestamp(), nonce());
            return xmlMapper.writeValueAsString(replyWrapper);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 计算消息签名
     *
     * @param encrypt 加密内容
     * @return 签名字符串
     * @throws WechatCallbackException 计算签名出错时抛出
     */
    public String signature(String encrypt) {
        try {
            String signatureString = Stream.of(token(), timestamp(), nonce(), encrypt)
                    .sorted()
                    .collect(Collectors.joining());
            return ShaKit.sha1().digest(signatureString)
                    .toHex();
        } catch (Exception e) {
            throw new WechatCallbackException(WechatCallbackErrorCode.COMPUTE_SIGNATURE_ERROR);
        }
    }

    /**
     * 解密回调消息的echo字符串。
     * 该方法首先生成echo字符串的签名，然后与传入的消息签名进行对比，如果签名不一致，则抛出微信回调异常。
     * 如果签名一致，则对echo字符串进行解密，并返回解密后的结果。
     *
     * @return 解密后的echo字符串。
     * @throws WechatCallbackException 如果消息签名验证失败，则抛出此异常。
     */
    public String decryptEcho(String echoStr) {
        // 生成echo字符串的签名
        String signature = signature(echoStr);

        // 验证消息签名是否一致
        if (!signature.equals(msgSignature())) {
            // 如果签名不一致，抛出签名验证错误异常
            throw new WechatCallbackException(WechatCallbackErrorCode.VALIDATE_SIGNATURE_ERROR);
        }

        // 对echo字符串进行解密并返回
        return decrypt(echoStr);
    }


    /**
     * 解密接收到的微信回调消息。
     * 该方法首先将接收到的加密消息字符串解析为WechatCallbackMessage对象，然后验证消息的签名，
     * 如果签名验证失败，则抛出WechatCallbackException异常。最后，对消息体进行解密并返回解密后的消息内容。
     *
     * @return 解密后的消息内容
     * @throws RuntimeException        如果JSON处理过程中发生异常
     * @throws WechatCallbackException 如果消息签名验证失败
     */
    public String decryptMessage(String encryptMessage) {
        try {
            // 创建XML映射器，用于从加密字符串解析WechatCallbackMessage对象
            XmlMapper xmlMapper = new XmlMapper();
            // 解析加密消息
            WechatCallbackMessage.ReceiveWrapper receiveWrapperMessage = xmlMapper.readValue(encryptMessage, WechatCallbackMessage.ReceiveWrapper.class);
            log.info("[WECHAT MESSAGE DECRYPT] Receive message toUserName is: {}", receiveWrapperMessage.toUserName());
            log.info("[WECHAT MESSAGE DECRYPT] Receive message encryptMessage is: {}", receiveWrapperMessage.encrypt());
            log.info("[WECHAT MESSAGE DECRYPT] Receive message agentId is: {}", receiveWrapperMessage.agentId());

            // 生成消息签名进行验证
            String signature = signature(receiveWrapperMessage.encrypt());
            log.info("[WECHAT MESSAGE DECRYPT]  signature is: {}", signature);
            // 比较接收到的消息签名和计算得到的消息签名，不一致则抛出异常
            if (!signature.equals(msgSignature())) {
                log.info("[WECHAT MESSAGE DECRYPT]  signature is not equal msgSignature,msgSignature is: {}", msgSignature());
                throw new WechatCallbackException(WechatCallbackErrorCode.VALIDATE_SIGNATURE_ERROR);
            }
            log.info("[WECHAT MESSAGE DECRYPT] encryptMessage is: {}", receiveWrapperMessage.encrypt());
            // 对消息体进行解密
            String decrypt = decrypt(receiveWrapperMessage.encrypt());
            log.info("[WECHAT MESSAGE DECRYPT]  decrypt is: {}", decrypt);
            return decrypt;
        } catch (JsonProcessingException e) {
            // 处理JSON解析异常，转换为运行时异常抛出
            throw new RuntimeException(e);
        }
    }

    /**
     * 解密内容
     * <p>
     * 此方法用于解密通过AES加密的内容，并验证解密内容的来源是否与预期的corpId一致。
     * 如果来源不一致，会抛出WechatCallbackException异常。
     *
     * @param encryptContent 需要解密的内容，应该是经过AES加密并进行base64编码的字符串。
     * @return 解密后的字符串。如果验证通过，返回解密后的文本内容。
     * @throws WechatCallbackException 如果解密过程中验证corpId不一致，则抛出此异常。
     */
    private String decrypt(String encryptContent) {

        return AesKit.builder(ALGORITHM)
                .keyBase64(aesKey())
                .build()
                .decrypt(encryptContent)
                .map(it->{
                    byte[] bytes = it.bytes();
                    // PKCS7解码，恢复被加密内容的原始字节数组
                    byte[] decodeBytes = CorpMessageKit.pkcs7Decode(bytes);
                    // 提取网络字节序的4字节
                    byte[] networkOrder = Arrays.copyOfRange(decodeBytes, 16, 20);
                    // 将网络字节序转换为Java字节序，得到JSON内容的长度
                    int jsonLength = CorpMessageKit.recoverNetworkBytesOrder(networkOrder);
                    // 提取并转换JSON部分为字符串
                    String jsonContent = new String(Arrays.copyOfRange(bytes, 20, 20 + jsonLength), StandardCharsets.UTF_8)
                            .trim();
                    // 提取并转换fromReceiveId部分为字符串
                    String fromReceiveId = new String(Arrays.copyOfRange(bytes, 20 + jsonLength, bytes.length),
                            StandardCharsets.UTF_8)
                            .trim();
                    // 检查corpId是否相同，不相同则抛出异常
                    if (!fromReceiveId.equals(corpId())) {
                        throw new WechatCallbackException(WechatCallbackErrorCode.VALIDATE_CORP_ID_ERROR);
                    }
                    // 返回解密后的JSON内容
                    return jsonContent;
                });
    }

}
